<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8"/>
  <title>货物分拣 3D 演示 - TTS</title>
  <style>
      body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
      #info { position: absolute; top: 10px; left: 10px; color:#fff; background: rgba(0,0,0,.6); padding: 8px 12px; border-radius:4px; }
  </style>
</head>
<body>
  <div id="info">
      <button id="btnPlay">暂停</button>
      <button id="btnSpeed">x1</button>
	  <span id="total">货物总数:0</span>
      <span id="stats">等待货物…</span>
  </div>

  <!-- CDN 引入，可换成本地文件 -->
  <script src="ThreeJs/js/three.js"></script>
  <script src="ThreeJs/js/OrbitControls.js"></script>
  
  <script type="module">
	
	const beltW=1;//输送线宽
	/* ---------- 分拣环 ---------- */
	const W=5,L=30;
	// 下横/前面z=0
	//const front = new Belt(L, 0, 0, 0);//中心位置
	// 上横/后面z<0
	//const back = new Belt(L, 0, -W, 0);
	// 右纵x>0
	//const right = new Belt(W, L/2-beltW/2, -W/2, Math.PI/2);
	// 左纵x<0
	//const left = new Belt(W, -(L/2-beltW/2), -W/2, Math.PI/2);
	// 中点坐标
	const mf=[0,0,0];//以分拣环前面横向分拣线中点为视图中心点
	const mb=[0,0,-W];
	const mr=[  L/2-beltW/2, 0, -W/2];
	const ml=[-(L/2-beltW/2),0, -W/2];
	// 四角坐标 左下,右下,右上,左上
	const e0=[-L/2+beltW/2, 0, 0];
	const e1=[ L/2-beltW/2, 0, 0];
	const e2=[ L/2-beltW/2, 0, -W];
	const e3=[-L/2+beltW/2, 0, -W];
	/* 分拣环四条输送段 */
	const beltsRing = [
		{name:'ring-front', type:'ring', len:L, pos:mf, rot:0,         endpoints:[e0,e1], dir:[ 1, 0, 0]},
		{name:'ring-right', type:'ring', len:W, pos:mr, rot:Math.PI/2, endpoints:[e1,e2], dir:[ 0, 0,-1]},
		{name:'ring-back' , type:'ring', len:L, pos:mb, rot:0,         endpoints:[e2,e3], dir:[-1, 0, 0]},
		{name:'ring-left' , type:'ring', len:W, pos:ml, rot:Math.PI/2, endpoints:[e3,e0], dir:[ 0, 0, 1]}
	];
	/* 导入口输送段 */
	const len_imp = 2;
	const beltsImp = [
		{name:'IMP-B-8', type:'imp', len:len_imp, pos:[-8, 0, -W-len_imp/2 ], rot:Math.PI/2, endpoints:[[-8,0,-W-len_imp],[-8,0,-W]], dir:[ 0,0, 1]},
		{name:'IMP-B-6', type:'imp', len:len_imp, pos:[-6, 0, -W-len_imp/2 ], rot:Math.PI/2, endpoints:[[-6,0,-W-len_imp],[-6,0,-W]], dir:[ 0,0, 1]},
		{name:'IMP-F-4', type:'imp', len:len_imp, pos:[-4, 0,    len_imp/2 ], rot:Math.PI/2, endpoints:[[-4,0,   len_imp],[-4,0, 0]], dir:[ 0,0,-1]},
		{name:'IMP-F-2', type:'imp', len:len_imp, pos:[-2, 0,    len_imp/2 ], rot:Math.PI/2, endpoints:[[-2,0,   len_imp],[-2,0, 0]], dir:[ 0,0,-1]},
		{name:'IMP-F+0', type:'imp', len:len_imp, pos:[ 0, 0,    len_imp/2 ], rot:Math.PI/2, endpoints:[[-0,0,   len_imp],[-0,0, 0]], dir:[ 0,0,-1]}
	];
	/* 货物输送带 以端点来计算 */
	const len_conveyor = 4;
	const beltsCV = [
		{name:'CV-B-8',   type:'conveyor', len:len_conveyor, pos:[-8, 0, -W-len_imp-len_conveyor/2             ], rot:Math.PI/2, endpoints:[[-8,0,-W-len_imp-len_conveyor  ],[-8,0,-W-len_imp             ]], dir:[ 0,0, 1]}
	];
	/* 货物进入段 */
	const beltsIn = [
		{name:'CV-B-6-1', type:'conveyor', len:len_conveyor, pos:[-6+(len_conveyor*Math.cos(Math.PI/4))/2, 0, -W-len_imp-(len_conveyor*Math.cos(Math.PI/4))/2 ], rot: Math.PI/4, endpoints:[[-6+(len_conveyor*Math.cos(Math.PI/4)),0,-W-len_imp-(len_conveyor*Math.cos(Math.PI/4))],[-6,0,-W-len_imp]], dir:[-1,0, 1]},
		
		{name:'CV-B-8-1', type:'conveyor', len:len_conveyor, pos:[-8-(len_conveyor*Math.cos(Math.PI/4))/2, 0, -W-len_imp-len_conveyor-(len_conveyor*Math.cos(Math.PI/4))/2 ], rot:-Math.PI/4, endpoints:[[-8-(len_conveyor*Math.cos(Math.PI/4)),0,-W-len_imp-len_conveyor-(len_conveyor*Math.cos(Math.PI/4))],[-8,0,-W-len_imp-len_conveyor]], dir:[ 1,0, 1]},
		{name:'CV-B-8-2', type:'conveyor', len:len_conveyor, pos:[-8, 0, -W-len_imp-len_conveyor-len_conveyor/2], rot:Math.PI/2, endpoints:[[-8,0,-W-len_imp-len_conveyor*2],[-8,0,-W-len_imp-len_conveyor]], dir:[ 0,0, 1]},
		
		{name:'CV-B-6',   type:'conveyor', len:len_conveyor, pos:[-6, 0, -W-len_imp-len_conveyor/2 ], rot:Math.PI/2, endpoints:[[-6,0,-W-len_imp-len_conveyor  ],[-6,0,-W-len_imp  ]], dir:[ 0,0, 1]},
		
		{name:'CV-F-4',   type:'conveyor', len:len_conveyor, pos:[-4, 0,    len_imp+len_conveyor/2 ], rot:Math.PI/2, endpoints:[[-4,0,   len_imp+len_conveyor  ],[-4,0,   len_imp  ]], dir:[ 0,0,-1]},
		{name:'CV-F-2',   type:'conveyor', len:len_conveyor, pos:[-2, 0,    len_imp+len_conveyor/2 ], rot:Math.PI/2, endpoints:[[-2,0,   len_imp+len_conveyor  ],[-2,0,   len_imp  ]], dir:[ 0,0,-1]},
		{name:'CV-F+0',   type:'conveyor', len:len_conveyor, pos:[ 0, 0,    len_imp+len_conveyor/2 ], rot:Math.PI/2, endpoints:[[-0,0,   len_imp+len_conveyor  ],[-0,0,   len_imp  ]], dir:[ 0,0,-1]}
	];
	/* ---------- 分拣出口 F前面 B后面 ---------- */
	const len_chute = 1.5;
	const beltsChute = [
		{name:'B+0', type:'chute', len:len_chute, pos:[ 0, 0, -W-len_chute/2], rot:Math.PI/2, endpoints:[[ 0,0,-W],[ 0,0,-W-len_chute]], dir:[ 0,0,-1]},
		{name:'B+2', type:'chute', len:len_chute, pos:[ 2, 0, -W-len_chute/2], rot:Math.PI/2, endpoints:[[ 2,0,-W],[ 2,0,-W-len_chute]], dir:[ 0,0,-1]},
		{name:'B+4', type:'chute', len:len_chute, pos:[ 4, 0, -W-len_chute/2], rot:Math.PI/2, endpoints:[[ 4,0,-W],[ 4,0,-W-len_chute]], dir:[ 0,0,-1]},
		{name:'F-8', type:'chute', len:len_chute, pos:[-8, 0,    len_chute/2], rot:Math.PI/2, endpoints:[[-8,0, 0],[-8,0,   len_chute]], dir:[ 0,0, 1]},
		{name:'F-6', type:'chute', len:len_chute, pos:[-6, 0,    len_chute/2], rot:Math.PI/2, endpoints:[[-6,0, 0],[-6,0,   len_chute]], dir:[ 0,0, 1]},
		{name:'F+2', type:'chute', len:len_chute, pos:[ 2, 0,    len_chute/2], rot:Math.PI/2, endpoints:[[ 2,0, 0],[ 2,0,   len_chute]], dir:[ 0,0, 1]},
		{name:'F+4', type:'chute', len:len_chute, pos:[ 4, 0,    len_chute/2], rot:Math.PI/2, endpoints:[[ 4,0, 0],[ 4,0,   len_chute]], dir:[ 0,0, 1]},
		{name:'F+6', type:'chute', len:len_chute, pos:[ 6, 0,    len_chute/2], rot:Math.PI/2, endpoints:[[ 6,0, 0],[ 6,0,   len_chute]], dir:[ 0,0, 1]},
		{name:'F+8', type:'chute', len:len_chute, pos:[ 8, 0,    len_chute/2], rot:Math.PI/2, endpoints:[[ 8,0, 0],[ 8,0,   len_chute]], dir:[ 0,0, 1]}
	];
	// 货物颜色与分拣口颜色对应,方便查看
	//const colors = [0xff0000, 0x00ff00, 0x0000ff,0xffaa00, 0x00ffaa, 0xaa00ff];
	const colors = [];
	const N = beltsChute.length;
	for (let i = 0; i < N; i++) {
		const hue = i / N;
		colors.push(new THREE.Color().setHSL(hue, 1, 0.5));
	}
 
	const belts = [...beltsRing,...beltsImp,...beltsCV,...beltsIn,...beltsChute];
	//beltsRing.push(...beltsChute);
	//beltsRing.push(...beltsImp);
	
	/* ---------- adj邻接表 divert分流,merge汇流,branch/side支线/侧边进入,jion是连接---------- */
	const edges = [
		{ from:"CV-B-8-1", to:"CV-B-8" ,type: "merge" },
		{ from:"CV-B-8-2", to:"CV-B-8" ,type: "merge" },
		{ from:"CV-B-6-1", to:"CV-B-6" ,type: "join" },
		{ from:"CV-B-8", to:"IMP-B-8" ,type: "join" },
		{ from:"CV-B-6", to:"IMP-B-6" ,type: "join" },
		{ from:"CV-F-4", to:"IMP-F-4" ,type: "join" },
		{ from:"CV-F-2", to:"IMP-F-2" ,type: "join" },
		{ from:"CV-F+0", to:"IMP-F+0" ,type: "join" },
		{ from:"IMP-B-8", to:"ring-back" ,type: "side" },
		{ from:"IMP-B-6", to:"ring-back" ,type: "side" },
		{ from:"IMP-F-4", to:"ring-front",type: "side" },
		{ from:"IMP-F-2", to:"ring-front",type: "side" },
		{ from:"IMP-F+0", to:"ring-front",type: "side" },
		{ from:"ring-front",to:"ring-right" ,type: "join" },
		{ from:"ring-right",to:"ring-back"  ,type: "join" },
		{ from:"ring-back", to:"ring-left"  ,type: "join" },
		{ from:"ring-left", to:"ring-front" ,type: "join" },
		{ from:"ring-front",to:"F-8" ,type: "divert" },
		{ from:"ring-front",to:"F-6" ,type: "divert" },
		{ from:"ring-front",to:"F+2" ,type: "divert" },
		{ from:"ring-front",to:"F+4" ,type: "divert" },
		{ from:"ring-front",to:"F+6" ,type: "divert" },
		{ from:"ring-front",to:"F+8" ,type: "divert" },
		{ from:"ring-back", to:"B+0" ,type: "divert" },
		{ from:"ring-back", to:"B+2" ,type: "divert" },
		{ from:"ring-back", to:"B+4" ,type: "divert" }
	];
	
	// 构建图结构 - 初始化时执行一次
	// { "ring-back": ["B+0","B+2","B+4"] }
	const graph = (function buildGraph(edges) {
		const graph = {};
		// 初始化所有节点
		edges.forEach(edge => {
			if (!graph[edge.from]) {
				graph[edge.from] = [];
			}
			if (!graph[edge.to]) {
				graph[edge.to] = [];
			}
		});
		// 添加双向连接
		edges.forEach(edge => {
			graph[edge.from].push(edge.to);
			//graph[edge.to].push(edge.from); // 假设连接是双向的
		});
		return graph;
	})(edges); // 立即执行函数，初始化时就构建图
	// 寻找从起点到终点的路径
	function findRoute(start, end) {
		if (!graph[start] || !graph[end]) {
			return null; // 节点不存在
		}
		
		// BFS算法实现
		const queue = [[start]];
		const visited = new Set();
		visited.add(start);
		
		while (queue.length > 0) {
			// 取出队列中的第一条路径
			const path = queue.shift();
			// 取出路径的最后一个节点
			const current = path[path.length - 1];
			// 如果到达终点，返回路径
			if (current === end) {
				return path;
			}
			// 遍历当前节点的所有邻居
			for (const neighbor of graph[current]) {
				if (!visited.has(neighbor)) {
					visited.add(neighbor);
					// 创建新路径并加入队列
					const newPath = [...path, neighbor];
					queue.push(newPath);
				}
			}
		}
		// 没有找到路径
		return null;
	}
	
	function distance(p1,p2){
		//2点间距离<0.12表示到达该位置, 是否优化为二维坐标的距离(x,z),或三维(x,y,z)
		//console.log(Math.abs(p1 -p2));
		return Math.abs(p1 -p2) < 0.08;
	}
	// 欧几里得距离
	function distanceEuclidean(p1, p2) {
		if (p1.length !== p2.length) return -1;
		if (p1.length === 1 ) return Math.abs(p1 - p2);
		if (p1.length === 2 ){
			const dx = p1[0] - p2[0];
			const dy = p1[1] - p2[1];
			return Math.sqrt(dx * dx + dy * dy);
		}
		if (p1.length === 3 ){
			const dx = p1[0] - p2[0];
			const dy = p1[1] - p2[1];
			const dz = p1[2] - p2[2];
			return Math.sqrt(dx * dx + dy * dy + dz * dz);
		}
		// 多维, 可替换前面的2,3(1也可以)
		let sumOfSquares = 0;
		for (let i = 0; i < p1.length; i++) {
			const diff = p2[i] - p1[i];
			sumOfSquares += diff * diff;
		}
		return Math.sqrt(sumOfSquares);
	}
  
  
	const scene    = new THREE.Scene();
	scene.background = new THREE.Color(0x202020);
	const camera   = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
	camera.position.set(5, 10, 15); // 调整相机位置以便更好地观察
	const renderer = new THREE.WebGLRenderer({ antialias: true });
	renderer.setSize(innerWidth, innerHeight);
	renderer.shadowMap.enabled = true;
	document.body.appendChild(renderer.domElement);

	const ctrl = new THREE.OrbitControls(camera, renderer.domElement);
	ctrl.enableDamping = true;

	/* ---------- 灯光 ---------- */
	const amb = new THREE.AmbientLight(0xffffff, 0.6);
	scene.add(amb);
	const dir = new THREE.DirectionalLight(0xffffff, 1);
	dir.position.set(5,10,7);
	dir.castShadow = true;
	scene.add(dir);
	
	// 创建射线投射器
	const raycaster = new THREE.Raycaster();
	// 创建用于存储鼠标位置的向量
	const mouse = new THREE.Vector2();

	/* ---------- 地面 ---------- */
	const groundGeo = new THREE.PlaneGeometry(30,30);
	const groundMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
	const ground = new THREE.Mesh(groundGeo, groundMat);
	ground.rotation.x = -Math.PI/2;
	ground.receiveShadow = true;
	//scene.add(ground);

	/* 显示坐标轴（红 X、绿 Y、蓝 Z） */
	const axes = new THREE.AxesHelper(10);
	//scene.add(axes);

	/* ---------- 传送带类 ---------- */
	class Belt {
		constructor(data, colors = [0x555555, 0x00ff00]) {
			//colors:[传送带颜色,中心线颜色]
			this.data = data;
			const x=data.pos[0], y=data.pos[1], z=data.pos[2];
			const geo = new THREE.BoxGeometry(data.len, 0.1, beltW);
			const mat = new THREE.MeshStandardMaterial({ color: colors[0] });
			const mesh = new THREE.Mesh(geo, mat);
			mesh.position.set(x, y, z);
			mesh.rotation.y = data.rot;
			mesh.receiveShadow = mesh.castShadow = false;
			scene.add(mesh);
			
			// 绘制传送带中心线
			//let pts = [];
			//if (data.rot === 0) {
			//	// 横向
			//	pts = [ [x - data.len/2, 0.11, z], [x + data.len/2, 0.11, z] ];
			//} else {
			//	// 纵向
			//	pts = [ [x, 0.11, z + data.len/2], [x, 0.11, z - data.len/2] ];
			//}
			const pts = [[data.endpoints[0][0],0.11,data.endpoints[0][2]],[data.endpoints[1][0],0.11,data.endpoints[1][2]]];
			
			const lineGeo = new THREE.BufferGeometry().setFromPoints( pts.map(p=>new THREE.Vector3(...p)) );
			const lineMat = new THREE.LineBasicMaterial({ color: colors[1] });
			const line = new THREE.Line(lineGeo, lineMat);
			scene.add(line);
		}
	}
	
	beltsRing.forEach((o,idx)=>{
		new Belt(o);
	});
	beltsImp.forEach((o,idx)=>{
		new Belt(o);
	});
	beltsCV.forEach((o,idx)=>{
		new Belt(o);
	});
	beltsIn.forEach((o,idx)=>{
		new Belt(o);
	});
	beltsChute.forEach((o,idx)=>{
		new Belt(o,[colors[idx],0xffffff]);
	});

	/* ---------- 包裹类 ---------- */
	class Parcel {
		constructor(id, start,end,color) {
			this.id = id;
			//this.end = beltsChute[endIdx];			// 终点:目标出口
			//this.start = beltsImp[startIdx];	// 起点:导入口
			//this.current=beltsImp[startIdx]; //当前位置
			//this.routes= [];				 //路由点
			this.start   = start;
			this.setCurrent(start);
			this.isMoving = true; // 新增：是否正在移动
			
			const mat = new THREE.MeshStandardMaterial({ color: color?color:0xcccccc });
			this.mesh = new THREE.Mesh(new THREE.BoxGeometry(0.6,0.6,0.6), mat);
			this.mesh.castShadow = false;
			this.mesh.position.set(this.start.endpoints[0][0], 0.4, this.start.endpoints[0][2]);
			this.mesh.userData = {
				id: id
			};
			scene.add(this.mesh);
			
			this.setEnd(end);
		}
		setEnd(end){
			if(!end) return;
			//设置目的地
			this.end = end;
			//路由点
			const routes = [];
			const paths = findRoute(this.start.name, end.name); //路由,从当前计算还是从开始计算?
			//console.log(paths);
			if(!paths) return;
			paths.forEach(x => {
				routes.push(belts.find(y => y.name === x));
			});
			this.routes= routes;				 //路由点
			//修改为目标颜色
			const endIdx = beltsChute.findIndex(item => item.name === end.name);
			this.mesh.material.color=colors[endIdx];
		}
		setCurrent(current){
			this.next = null;//重置下个节点
			this.current = current;
			if(!current) return;
			//是最后一个
			if(this.end && this.end.name === current.name){
				return;
			}
			//有路由
			if(this.routes){
				const idx = this.routes.findIndex(item => item.name === current.name);
				if (idx>=0 && idx < this.routes.length-1){
					this.next = this.routes[idx+1];
				}
			}
			if(!this.next){
				//根据物理位置选后一个节点
				const nextName = edges.find(x => x.from === current.name).to;
				this.next = belts.find(y => y.name === nextName);
			}
		}
		
		checkConflict() {
			// 检查分拣环入口是否有冲突包裹. 需优化:分汇入(汇入线等待) 与 合流(2条线交替等待)
			////const edge = edges.find(x => x.from === this.current.name); //合流线
			////const targetName = edge?edge.to:"";
			const targetName = edges.find(x => x.from === this.current.name).to; //合流线
			for (const [id, parcel] of parcels) {
				if (parcel.id === this.id || parcel.current.name !== targetName) continue;
				//console.log(this.current.pos[0] + ' ' +parcel.id+ ' '+ parcel.state + ' ' +parcel.mesh.position.x);
				if (Math.abs(parcel.mesh.position.x-this.current.pos[0])<speed/2) {
					return true;
				}
			}
			return false;
		}
		checkCollision(otherParcel) {
			// 检测碰撞
			const dist = this.mesh.position.distanceTo(otherParcel.mesh.position);
			return dist < 0.8; // 小于0.8视为即将碰撞
		}
		checkCollision2(otherParcel) {
			// 检测碰撞2, 应该是找最近的第一个和第二个,之间的间隔足够当前包裹进入
			// 1.已经注入的不检测了
			const dist0 = distanceEuclidean([this.mesh.position.x,this.mesh.position.y-0.4,this.mesh.position.z],[this.current.endpoints[1][0],this.current.endpoints[1][1]-0.4,this.current.endpoints[1][2]]);
			if(dist0<speed) return false;
			// 2.汇流的与主线的包裹将来会碰撞
			let x1,y1,z1;
			if(this.current.dir[0] === 11) x1 = this.mesh.position.x + speed;
			else if(this.current.dir[0] === -11) x1 = this.mesh.position.x - speed;
			else x1 = this.mesh.position.x;
			
			y1 = this.mesh.position.y;
			
			if(this.current.dir[2] === 11) z1 = this.mesh.position.z + speed;
			else if(this.current.dir[2] === -11) z1 = this.mesh.position.z - speed;
			else z1 = this.mesh.position.z;
			
			let x2,y2,z2;
			if(otherParcel.current.dir[0] === 1) x2 = otherParcel.mesh.position.x + speed;
			else if(otherParcel.current.dir[0] === -1) x2 = otherParcel.mesh.position.x - speed;
			else x2 = otherParcel.mesh.position.x;
			
			y2 = otherParcel.mesh.position.y;
			
			if(otherParcel.current.dir[2] === 1) z2 = otherParcel.mesh.position.z + speed;
			else if(otherParcel.current.dir[2] === -1) z2 = otherParcel.mesh.position.z - speed;
			else z2 = otherParcel.mesh.position.z;
			
			const dist = distanceEuclidean([x1,y1-0.4,z1],[x2,y2-0.4,z2]);
			return dist < speed + 0.5;
		}
		checkCoincide(otherPos){
			// 检测重合 (0.4是包裹中心位置)
			//console.log(dist + " [" + this.mesh.position.x +','+this.mesh.position.y +','+this.mesh.position.z + '] [' + otherPos + ']');
			const dist = distanceEuclidean([this.mesh.position.x,this.mesh.position.y-0.4,this.mesh.position.z],otherPos);
			return dist < 0.08; // 小于0.08视为重合
		}
		check2div(){
			//到达分流
			return this.checkCoincide(this.next.endpoints[0]); //计算立面
			//return distance(this.mesh.position.x,this.next.endpoints[0][0]) && distance(this.mesh.position.z,this.next.endpoints[0][2]); //计算平面
		}
		check2end(){
			//到达段末尾
			//return this.checkCoincide(this.current.endpoints[1]);
			return   (this.current.dir[0] ===  1 && this.mesh.position.x >= this.current.endpoints[1][0]) 
				  || (this.current.dir[1] ===  1 && this.mesh.position.y-0.4 >= this.current.endpoints[1][1])
				  || (this.current.dir[2] ===  1 && this.mesh.position.z >= this.current.endpoints[1][2])
				  || (this.current.dir[0] === -1 && this.mesh.position.x <= this.current.endpoints[1][0])
				  || (this.current.dir[1] === -1 && this.mesh.position.y-0.4 <= this.current.endpoints[1][1])
				  || (this.current.dir[2] === -1 && this.mesh.position.z <= this.current.endpoints[1][2])
				;
		}
		updateLine(){
			//包裹到目标位置连线
			if (this.mesh.material.emissive.r === 1) {
				//选中的
				if(this.line){
					//有连线,则更新
					const positions = this.line.geometry.attributes.position.array;
					positions[0] = this.mesh.position.x;
					positions[1] = this.mesh.position.y;
					positions[2] = this.mesh.position.z;
					this.line.geometry.attributes.position.needsUpdate = true;
				}else if(this.end){
					//没连线 && 有目标位置 -> 则创建连线
					const pts = [[this.mesh.position.x,this.mesh.position.y,this.mesh.position.z],this.end.endpoints[1]];
					const lineGeo = new THREE.BufferGeometry().setFromPoints( pts.map(p=>new THREE.Vector3(...p)) );
					const lineMat = new THREE.LineBasicMaterial({ color: 0xcccccc });
					const line = new THREE.Line(lineGeo, lineMat);
					scene.add(line);
					this.line = line;
				}
				
			} else {
				//目标位置连线取消
				if(this.line){
					scene.remove(this.line);
					this.line = null;
				}
			}
		}
	  
		update(delta) {
			if (!this.isMoving) return false;
			const distance_move = 2 * delta * speed; //每次刷新时移动的距离
			//console.log('['+this.mesh.position.x + ',' +this.mesh.position.y+ ','+this.mesh.position.z + '] dir:'+ this.current.dir + " "+ this.current.name);
			//console.log(this.current.name + ' [' +this.current.dir+ '] , '+this.next.name + ' ['+ this.next.dir + "]");
			if (this.current.dir[0] ===  1) this.mesh.position.x += distance_move;
			if (this.current.dir[1] ===  1) this.mesh.position.y += distance_move;
			if (this.current.dir[2] ===  1) this.mesh.position.z += distance_move;
			if (this.current.dir[0] === -1) this.mesh.position.x -= distance_move;
			if (this.current.dir[1] === -1) this.mesh.position.y -= distance_move;
			if (this.current.dir[2] === -1) this.mesh.position.z -= distance_move;
			
			this.updateLine();
			
			if (this.next && edges.find(x => x.from === this.current.name && x.to === this.next.name && x.type === "divert")) {
				// 段中分流 分流检测		下个点就是当前段的分流点
				if(this.check2div()){
					this.setCurrent(this.next);
					return;
				}
				//console.log(this.current.name + ' , '+this.next.name);
			}
			//if (this.current.type === 'ring') {
				//const edge = edges.find(x => x.from === this.current.name && x.to === this.end.name); //下个点就是当前段的分流点
				//if(edge && this.distance(this.mesh.position.x,this.end.pos[0])){
				//	// 分拣口
				//	this.current = this.end;
				//	return;
				//}
			//	if(this.current.name === "ring-front"){
			//		// 分拣口
			//		if (this.end.side==='z' && this.distance(this.mesh.position.x,this.end.pos[0])) {
			//			this.current = this.end;
			//			return;
			//		}
			//	}
			//	else if (this.current.name === 'ring-back') {
			//		// 分拣口
			//		if (this.end.side==='-z' && this.distance(this.mesh.position.x,this.end.pos[0])) {
			//			this.current = this.end;
			//			return;
			//		}
			//	}
			//} 
			
			// 到达段末尾
			if ( this.check2end()){
				//console.log(this.current.name + ' [' +this.current.dir+ '] end');
				if(!this.next){
					if(this.line) scene.remove(this.line);
					scene.remove(this.mesh);
					return true; // 完成,包裹移除
				}
				if (this.current.type === 'imp'){
					// 导入口结束,如果没有目的地,则生成目的地
					if(!this.end){
						const targetIdx = Math.floor(Math.random()*beltsChute.length);
						this.setEnd(beltsChute[targetIdx]);
					}
				}
				//console.log(this.current.name + ' [' +this.current.dir+ '] , '+this.next.name + ' ['+ this.next.dir + "]");
				if (this.current.type === 'ring' && this.next.type !== 'ring'){
					// 环循环
					//console.log("ring");
					const to = edges.find(x => x.from === this.current.name && x.to.startsWith("ring-")).to;//环连接线 x.type === "join"
					this.setCurrent(beltsRing.find(x => x.name === to));
				} else {
					//console.log("next");
					this.setCurrent(this.next);
				}
				
				/*
				if (this.current.type === 'imp'){
					// 导入口
					const to = edges.find(x => x.from === this.current.name).to; //合流线
					this.current = beltsRing.find(x => x.name === to);
					//if (this.current.side === 'lu') this.current = beltsRing[2];
					//else if (this.current.side === 'rd') this.current = beltsRing[0];
				}
				else if (this.current.type === 'ring'){
					// 分拣环
					const to = edges.find(x => x.from === this.current.name && x.to.startsWith("ring-")).to;//区分分拣口
					this.current = beltsRing.find(x => x.name === to);
					
					//if(this.current.name === "ring-front"){
					//	// 环右面
					//	this.current = beltsRing[1];
					//} else if (this.current.name === 'ring-right') {
					//	// 环后面
					//	this.current = beltsRing[2];
					//} else if (this.current.name === 'ring-back') {
					//	// 环左面
					//	this.current = beltsRing[3];
					//} else if (this.current.name === 'ring-left') {
					//	// 环前面
					//	this.current = beltsRing[0];
					//}
				}
				else if (this.current.type === 'chute') {
					// 分拣口
					scene.remove(this.mesh);
					return true; // 完成,包裹移除
				}
				*/
			}
			return false;
		}
	}
	
	
	// 检查current是否在other后面（根据输送方向）
	function checkIsBehind(self, other) {
		const posSelf = self.mesh.position;
		const posOther = other.mesh.position;
		//console.log(self.current.name + " dir:"+ dir + ", [" + self.mesh.position.x +','+self.mesh.position.y +','+self.mesh.position.z + ']');
		//console.log(other.current.name + " dir:"+ dir + ", [" + other.mesh.position.x +','+other.mesh.position.y +','+other.mesh.position.z + ']');
		// 根据传送带方向判断前后关系
		if (self.current.dir[0] ===  1 ) return posSelf.x < posOther.x; // 沿X正方向
		if (self.current.dir[0] === -1 ) return posSelf.x > posOther.x; // 沿X负方向
		if (self.current.dir[2] ===  1 ) return posSelf.z < posOther.z; // 沿Z正方向
		if (self.current.dir[2] === -1 ) return posSelf.z > posOther.z; // 沿Z负方向
		return false;
	}
	//self当前位置是否比other当前位置距离next起点更远
	function far2Next(self, other, next){
		const self2next  = distanceEuclidean([ self.mesh.position.x, self.mesh.position.y-0.4, self.mesh.position.z],next.endpoints[0]);
		const other2next = distanceEuclidean([other.mesh.position.x,other.mesh.position.y-0.4,other.mesh.position.z],next.endpoints[0]);
		return self2next>other2next;
	}

	/* ---------- 主循环 ---------- */
	const parcels = new Map();
	let paused = false;
	let speed  = 1;
	const clock = new THREE.Clock();

	function animate() {
		requestAnimationFrame(animate);
		if (!paused) {
			const delta = clock.getDelta();
			if(delta>0.1) return; //浏览器切换到后台,则不运行动画; 后台运行的话,包裹的位置可能超出了输送
			// 1. 先重置所有包裹的移动状态
			parcels.forEach(p => {
			  p.isMoving = true;
			});
			// 2. 检测碰撞并设置等待状态
			const parcelList = Array.from(parcels.values());
			for (let i = 0; i < parcelList.length; i++) {
				const self = parcelList[i];
				
				// 同一输送方向前后衔接输送段的包裹
				const parcelsSameOrNext = Array.from(parcels.values()).filter(p => p.id !== self.id 
					&& (p.current.name === self.current.name || (self.next && self.next === p.current)) );
				for (let j = 0; j < parcelsSameOrNext.length; j++){
					const other = parcelsSameOrNext[j];
					if (self.checkCollision(other)) {
					  // 后面的包裹需要等待
					  const isBehind = checkIsBehind(self, other);
					  //console.log(self.current.name + ' ' + other.current.name +' '+isBehind);
					  if (isBehind) {
						self.isMoving = false;
						break; // 找到一个碰撞对象即可
					  }
					}
				}
				if(!self.next) continue;
				if(!self.isMoving) continue;
				// 合流线上的包裹
				const parcelsMerge = Array.from(parcels.values()).filter(p => p.id !== self.id && (p.next && self.next.name === p.next.name) );
				for (let j = 0; j < parcelsMerge.length; j++){
					const other = parcelsMerge[j];
					//2个包裹都在同一个合流线上
					const edgeSelf  = edges.find(x => x.from === self.current.name && x.to === self.next.name && x.type === "merge" ); 
					const edgeOther = edges.find(x => x.from ===other.current.name && x.to ===other.next.name && x.type === "merge" ); 
					if(edgeSelf && edgeOther && self.checkCollision(other)){
						// 包裹将碰撞
						const isBehind = far2Next(self, other, self.next);
						//console.log(self.current.name + ' ' + other.current.name +' '+isBehind);
						if (isBehind) {
							self.isMoving = false;
							break; // 找到一个碰撞对象即可
						}
					}
				}
				if(!self.isMoving) continue;
				// 汇流线上包裹(支路停等,主路不停) TODO: 优化队列,减少已过汇流点后的包裹
				const parcelsSide = Array.from(parcels.values()).filter(p => p.id !== self.id && self.next.name === p.current.name );
				for (let j = 0; j < parcelsSide.length; j++){
					const other = parcelsSide[j];
					//当前包裹汇流到另外那个包裹所在线上
					const edgeSelf  = edges.find(x => x.from === self.current.name && x.to === self.next.name && x.type === "side" ); 
					if(edgeSelf && self.checkCollision2(other)){
						// 包裹将碰撞
						self.isMoving = false;
						break;
					}
				}
			  /*
			  // 检查当前包裹是否需要等待
			  for (let j = 0; j < parcelList.length; j++) {
				if (i === j) continue;
				const other = parcelList[j];
				
				// 同一输送段或前后衔接输送段的包裹
				const sameOrNext = self.current === other.current || (self.next && self.next === other.current);
				if (sameOrNext && self.checkCollision(other)) {
				  // 后面的包裹需要等待
				  const isBehind = checkIsBehind(self, other);
				  //console.log(self.current.name + ' ' + other.current.name +' '+isBehind);
				  if (isBehind) {
					self.isMoving = false;
					break; // 找到一个碰撞对象即可
				  }
				}
				// todo:先过滤所有汇合流线的包裹
				// 合流线上的包裹
				if(!sameOrNext && self.next && other.next && self.next.name === other.next.name){
					//2个包裹都在同一个合流线上
					const edgeSelf  = edges.find(x => x.from === self.current.name && x.to === self.next.name && x.type === "merge" ); 
					const edgeOther = edges.find(x => x.from ===other.current.name && x.to ===other.next.name && x.type === "merge" ); 
					if(edgeSelf && edgeOther && self.checkCollision(other)){
						// 包裹将碰撞
						const isBehind = far2Next(self, other, self.next);
						//console.log(self.current.name + ' ' + other.current.name +' '+isBehind);
						if (isBehind) {
							self.isMoving = false;
							break; // 找到一个碰撞对象即可
						}
					}
				}
				// 汇流线上包裹(支路停等,主路不停)
				if((self.next && self.next === other.current)){
					//当前包裹汇流到另外那个包裹所在线上
					const edgeSelf  = edges.find(x => x.from === self.current.name && x.to === self.next.name && x.type === "side" ); 
					if(edgeSelf && self.checkCollision2(other)){
						// 包裹将碰撞
						self.isMoving = false;
						break;
					}
				}
			  }
			  */
			}
			// 3. 更新所有包裹位置
			for (let [id, p] of parcels) {
				const finished = p.update(delta);
				if (finished) parcels.delete(id);
			}
			document.getElementById('stats').textContent = `在线货物: ${parcels.size}`;
		}
		ctrl.update();
		renderer.render(scene, camera);
	}
	animate();
	
	/* ---------- 鼠标点击 ---------- */
	document.addEventListener('click', onMouseClick, false);
	function onMouseClick(event) {
		// 将鼠标位置归一化，使其在[-1, 1]范围内
		mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
		mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

		// 更新射线投射器
		raycaster.setFromCamera(mouse, camera);
		
		const parcelMesh = Array.from(parcels.values()).map(x=>x.mesh);
		const intersects = raycaster.intersectObjects(parcelMesh);
		if(intersects.length > 0){
			toggleHighlight(intersects[0].object);
		}
		
//		// 检测与场景中物体的交点
//		const intersects = raycaster.intersectObjects(scene.children);
//
//		// 遍历所有交点，判断是否点击到立方体
//		intersects.forEach(intersect => {
//			console.log(intersect);
//			if (intersect.object.type === "Mesh") {
//				toggleHighlight(intersect.object);
//			}
//		});
	}

	function toggleHighlight(mesh) {
		const parcel = parcels.get(mesh.userData.id);
		const material = mesh.material;

		// 判断当前材质是否为高亮状态
		if (material.emissive.r === 0) {
			// 设置高亮效果
			material.emissive.setRGB(1, 1, 1);
			//material.shininess = 100;
			
			//目标位置连线
			if(parcel.end){
				const pts = [[mesh.position.x,mesh.position.y,mesh.position.z],parcel.end.endpoints[1]];
				const lineGeo = new THREE.BufferGeometry().setFromPoints( pts.map(p=>new THREE.Vector3(...p)) );
				const lineMat = new THREE.LineBasicMaterial({ color: 0xcccccc });
				const line = new THREE.Line(lineGeo, lineMat);
				scene.add(line);
				parcel.line = line;
		//console.log(parcel);
			}
			
		} else {
			// 恢复原状态
			material.emissive.setRGB(0, 0, 0);
			//material.shininess = 30;
			
			//目标位置连线取消
			if(parcel.line){
				scene.remove(parcel.line);
				parcel.line = null;
			}
		}
	}

	/* ---------- 控制按钮 ---------- */
	document.getElementById('btnPlay').onclick = () => {
		paused = !paused;
		if(paused) clock.stop(); else clock.start();
		document.getElementById('btnPlay').textContent = paused ? '播放' : '暂停';
	};
	document.getElementById('btnSpeed').onclick = () => {
		speed = speed === 1 ? 2 : speed === 2 ? 4 : speed === 4 ? 8 : speed === 8 ? 0.5 : 1;
		document.getElementById('btnSpeed').textContent = `x${speed}`;
	};

	/* ---------- 自动生成测试数据 ---------- */
	function createParcel(Id,start,end,color){
		
		parcels.set(Id, new Parcel(Id, start));
		document.getElementById('total').textContent = `货物总数: ${Id} `;
	}
	let testId = 0;
	function generateParcel(){
		if (!paused) {
			const impIdx = Math.floor(Math.random()*beltsIn.length);
			const targetIdx = Math.floor(Math.random()*beltsChute.length);
			const color = colors[targetIdx];
			
			// 有等待则不生成新包裹
			//const waitingParcels = Array.from(parcels.values()).filter(p => p.current.name === beltsIn[impIdx].name && !p.isMoving).length; 
			//if(waitingParcels >0) return;
			
			// 每个进入段最多同时存在n个包裹(进入段长度len_conveyor)
			const startParcels = Array.from(parcels.values()).filter(p => p.current.name === beltsIn[impIdx].name).length; 
			if (startParcels < 3) {
			  createParcel(testId, beltsIn[impIdx], beltsChute[targetIdx], color);
			  testId++;
			}
		}
	}
	//const cos45 = Math.cos(Math.PI / 4);
	//console.log(10*cos45);
	
	//createParcel(1, "CV-B-8-1", "F+2",colors[2]);
	//createParcel(2, "CV-B-8-2", "B+0",colors[6]);
	//createParcel(1, "IMP-B-6", "F-8",colors[0]);
	//createParcel(2, "IMP-F-4", "F-6",colors[1]);
	//createParcel(3, "IMP-B-8", "F+2",colors[2]);
	//createParcel(4, "IMP-F-2", "F+4",colors[3]);
	//createParcel(5, "IMP-B-6", "B+0",colors[6]);
	//createParcel(6, "IMP-F+0", "B+2",colors[7]);
	//setTimeout(createParcel, 1000, 3, 3, 4);
	setInterval(()=>generateParcel(), 500);

	/* ---------- 窗口适配 ---------- */
	addEventListener('resize', ()=>{
	  camera.aspect = innerWidth/innerHeight;
	  camera.updateProjectionMatrix();
	  renderer.setSize(innerWidth, innerHeight);
	});
  </script>
</body>
</html>